"""

General utility classes and functions

$Rev: 30 $ 
$Author: ialbert $ 
$Date: 2007-12-03 16:51:42 -0500 (Mon, 03 Dec 2007) $
"""

import functools
import time, os, tempfile, random, glob, sys, csv, gc, re
from itertools import count, izip

def join_paths(*args):
    "Joins and normalizes a path"
    return os.path.abspath( os.path.join(*args) )

def getdir( path ):
    """
    Returns the directory portion of a path
    """
    return os.path.split( path )[0]

def safe_max( data, default=1 ):
    """
    Safe max, that can handle an empty list
    >>> safe_max( [1,2,3 ] )
    3
    >>> safe_max( [] )
    1
    """
    try:
        return max(data)
    except ValueError:
        return default

def nice_sort( data ):
    """
    Sort the given list data in the way that humans expect. 
    Adapted from a posting by Ned Batchelder: http://nedbatchelder.com/blog/200712.html#e20071211T054956
    
    >>> data = [ 'chr1', 'chr2', 'chr10', 'chr100' ]
    >>> data.sort()
    >>> data
    ['chr1', 'chr10', 'chr100', 'chr2']
    >>> nice_sort(data)
    >>> data
    ['chr1', 'chr2', 'chr10', 'chr100']
    """
    def convert(text): 
        if text.isdigit():
            return int(text)
        else:    
            return text
            
    split    = lambda key: re.split('([0-9]+)', key)
    alphanum = lambda key: map(convert, split(key) )
    data.sort( key=alphanum )

def commify(n):
    """
    Formats numbers with commas

    >>> commify(10000)
    '10,000'
    """
    n = str(n)
    while True:
        (n, count) = re.subn(r'^([-+]?\d+)(\d{3})', r'\1,\2', n)
        if count == 0: 
            break
    return n 

#
# debug mode will disable the STOP_EXCEPTION to allow 
# one to pinpoint the source of the error
#
def debug_mode( flag=True):
    global STOP_EXCEPTION
    if flag:
        STOP_EXCEPTION = None
    else:
        STOP_EXCEPTION = Exception

debug_on  = functools.partial(debug_mode, True)
debug_off = functools.partial(debug_mode, False)

# default debug mode is off
debug_off()

def stop_on_error( func ):
    """ 
    A decorator that stops the execution if there was an exception.
    Will also hide the actual source of the error. Run debug_off() to
    get full tracebacks.

    >>> @stop_on_error
    ... def foo():
    ...    pass
    """
    global STOP_EXCEPTION
    
    @functools.wraps(func)
    def stop( *args,**kargs ):
        try:
            result = func( *args, **kargs)
        except STOP_EXCEPTION, exc:
            sys.stderr.write( 'ERROR in %s: %s\n' % (func.func_name, exc) )
            sys.stderr.flush()
            sys.exit()
        return result
    return stop

def gc_off( func ):
    """ 
    A decorator that turns the off the garbage collector 
    during the wrapped function

    >>> @gc_off
    ... def foo():
    ...    pass
    """
    @functools.wraps(func)
    def newfunc(*args,**kargs):
        try:
            gc.disable()
            result = func( *args, **kargs)
        finally:
            gc.enable()
        return result
    return newfunc

class CommentedFile:
    """
    A file reader that skips commented lines but 
    maintains correct line numbers
    """
    def __init__(self, fp):
        if isinstance(fp, str):
            fp = file(fp, 'rU')
        self.fp = fp
        self.linecount = 0

    def next(self):
        line = self.fp.next()
        self.linecount += 1
        while line.startswith('#'):
            line = self.fp.next()
            self.linecount += 1
        return line

    def __iter__(self):
        return self

    def close(self):
        self.fp.close()

def dict_reader( fname, delimiter='\t' ):
    """
    Reads delimited file, returns the linenumber and elements of each row
    """
    stream = CommentedFile( fname )
    reader = csv.DictReader( stream, delimiter=delimiter )
    for row in reader:
        yield stream.linecount, row

def tab_reader( fname, delimiter='\t' ):
    """
    Reads delimited file, returns the linenumber and elements of each row
    """
    stream = CommentedFile( fname )
    reader = csv.reader( stream, delimiter=delimiter )
    for row in reader:
        yield stream.linecount, row


class Tick(object):
    """
    A simple timer object to return elapsed times.

    >>> tick = Tick()
    >>> tick.format(30)
    '30.0 seconds'
    >>> tick.format(320)
    '5.3 minutes'
    >>> tick.format(3200)
    '53.3 minutes'
    >>> tick.format(30500)
    '8.5 hours'

    """
    def __init__(self):
        self.tick()

    def tick(self):
        self.start = time.time()

    def format(self, value):
        min1 = 60.0
        hour = 60 * min1
        if value < 60:
            return '%3.1f seconds' % value
        elif value < hour:
            return '%3.1f minutes' % (value/min1)
        else:
            return '%3.1f hours' % (value/hour)

    def tock(self):
        elapsed = time.time() - self.start
        self.tick()
        return self.format( elapsed )


def make_tempfile( fname=None, dir='', prefix='img', suffix='.png'):
    """
    Returns a filename and filepath to a temporary file
    
    If the {tid} parameter is not specified it will generate a random id
    >>> make_tempfile(fname=1)[0]
    'img1.png'
    >>> len(make_tempfile())
    2
    """
    
    if fname == None:
        if dir:
            fd, fpath = tempfile.mkstemp( suffix=suffix, prefix=prefix, dir=dir, text='wb')
        else:
            fd, fpath = tempfile.mkstemp( suffix=suffix, prefix=prefix, text='wb')
        os.close(fd)
        head, fname = os.path.split( fpath )
    else:
        fname = '%s%s%s' % (prefix, fname, suffix)
        fpath = join_paths( dir, fname )

    return fname, fpath

def clear_tempdir(dir, chance=10, days=1, patt='*.png', logger=lambda x:x ):
    """
    Clears the temporary directory with a 'chance' (percent) and for 
    files older than 'days'

    >>> clear_tempdir(dir='nowhere/')
    """
    
    if random.random() * 100 <= chance:
        fpatts = join_paths( dir, patt )
        fnames = glob.glob( fpatts )
        now, maxage = time.time(), 3600 * 24 * days
        
        def oldfiles( fname ):
            mtime = os.stat( fname ).st_mtime 
            return (now - mtime) > maxage

        try:
            removed = map( os.remove, filter( oldfiles, fnames) )
            logger("clearing cache, deleting %d files matching '%s'" % ( len(removed), fpatts ) )
        except OSError:
            # another process most likely
            logger('file removal failed') 
   
def test(verbose=0):
    """
    Main testrunnner
    """
    import doctest
    doctest.testmod( optionflags=doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE, verbose=verbose )

if __name__ == '__main__':
    test(verbose=0)

